简介
- 基于Spring的应用程序提供声明式安全保护的安全性框架,提供了完整的安全性解决方案,能够在web请求级别和方法调用级别处理身份验证和授权,充分使用了依赖注入和面向切面的技术。
- 主要从两个方面解决安全性问题
- web请求级别:使用servlet过滤器保护web请求并限制url的访问
- 方法调用级别:使用Spring AOP保护方法调用,确保具有适当权限的用户才能访问
- 总结:访问url时首先判断是否登录,其次用户是否具有权限访问。
SpringMvc整合Spring Security
pom.xml添加依赖
<!--版本管理,只需管理这个依赖即可使所有spring security的依赖版本统一--> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-bom</artifactId> <version>5.2.0.BUILD-SNAPSHOT</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <!--最少的版本--> <dependencies> <!--过滤器:身份验证与url接入控制--> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> </dependency> <!--包含命名空间,springMVC整合时需要在配置文件中配置spring security,所以这个必须要--> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <!--版本是n.n则命名空间也得一样--> </dependency> </dependencies>
还有一个单点登录,有时也需要
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-cas</artifactId> </dependency>
- 整合方法:
- 配置文件实现:在配置文件中指定拦截的url、所需权限、配置userDetailsService指定用户名密码对应权限即可
- Java Configuration:以下可以只选一个部分来用java实现,其他可由配置文件代替,毕竟不是所有的模块都需要自定义。
web.xml中声明代理servlet过滤器。原本的实现是配置一系列相关的
,使得配置文件臃肿难以阅读,而Spring security提供的代理servlet过滤器功能可以解决该问题。 <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class> org.springframework.web.filter.DelegatingFilterProxy </filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
- 其中,DelegatingFilterProxy是一个代理的servlet过滤器,它主要负责将工作委托给javax.servlet.Filter实现类,这个实现类作为一个
已经注册在Spring上下文中,其id便是上面 的名字。也即它会自动到Spring容器查找名为filter-name的bean并把所有Filter的操作委托给它,所以如果不是默认注入到Spring容器的bean,就需要手动注入。 applicationContext.xml中配置拦截请求
//添加命名空间 xmlns:sec="http://www.springframework.org/schema/security" xsi:http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.0.xsd" //对应版本是5.2.xsd才对 <!-- 配置为none的不经过任何spring的过滤器,主要是静态资源,我们不需要经过过滤器 --> <http pattern="/resources/**" security="none" /> <http pattern="/sitemap.xml" security="none" /> <http pattern="/favicon.ico" security="none" /> <!--或者--> <security:http use-expressions="true"> <!-- 顺序不能乱 ,按照从上往下依次过滤 --> <security:intercept-url pattern="/ps_service/index.jsp" access="permitAll"/> <security:intercept-url pattern="/ps_service/login.do" access="permitAll"/> <security:intercept-url pattern="/ps_service/loginfailed.do" access="permitAll"/> <security:intercept-url pattern="/cs/404.html" access="permitAll"/> <!--过滤浏览器自动发起的链接--> <security:intercept-url pattern="/favicon.ico"access="permitAll"/> </security:http> <!-- auto-config: true自动加载,自动生成一个默认登陆页面,url:localhost:8080/project/login; 如果不使用该属性 则默认为http-basic(没有session).;一般都是true了 lowercase-comparisons:表示URL比较前先转为小写。 path-type:表示使用Apache Ant的匹配模式。 access-denied-page:访问拒绝时转向的页面。 access-decision-manager-ref:指定了自定义的访问策略管理器。当系统角色名的前缀不是默认的ROLE_时,需要自定义访问策略管理器。 --> <sec:http auto-config="true" servlet-api-provision="false" lowercase-comparisons="false" access-denied-page="/html/error_page_access_denied.html" path-type="ant" access-decision-manager-ref="accessDecisionManager"> <!-- login-page:指定登录页面。 login-processing-url:指定了客户在登录页面中按下 Sign In 按钮时要访问的 URL。与登录页面form的action一致。其默认值为:/j_spring_security_check。 authentication-failure-url:登录的用户没有指定权限时跳转到的页面;指定权限在第120行 default-target-url:登录成功呈现给用户的页面。 always-use-default-target:指定了是否在身份验证通过后总是跳转到default-target-url属性指定的URL。 --> <sec:form-login login-page="/admin/page!login.action" login-processing-url="/admin/login" default-target-url="/admin/page!main.action" authentication-failure-url="/admin/page!login.action" authentication-success-handler-ref="myAuthenticationSuccessHandler" always-use-default-target="true" /> <!-- "记住我"功能,采用持久化策略(将用户的登录信息存放在数据库表中) --> <sec:remember-me data-source-ref="dataSource" key="e37f8888-0ooo-22dd-bd0b-9900211c9a66" /> <!-- logout-url:指定了用于响应退出系统请求的URL,即注销页面。其默认值为:/j_spring_security_logout。下例与与 <a href="<c:url value="/admin/logout" />">注销</a> 配合使用 logout-success-url:退出系统后转向的URL,即注销成功后跳转到的页面。 invalidate-session:指定在退出系统时是否要销毁Session。 --> <sec:logout invalidate-session="true" logout-success-url="/admin/page!login.action" logout-url="/admin/logout" /> <!-- max-sessions:允许用户帐号登录的次数。范例限制用户只能登录一次。 exception-if-maximum-exceeded: 默认为false,此值表示:用户第二次登录时,前一次的登录信息都被清空。 当exception-if-maximum-exceeded="true"时系统会拒绝第二次登录。 --> <sec:concurrent-session-control max-sessions="1" exception-if-maximum-exceeded="false" /> <!-- 后台登录 --> <!--intercept-url:拦截器,可以设定哪些路径需要哪些权限来访问. filters=none 不使用过滤,也可以理解为忽略--> <sec:intercept-url pattern="/admin/page!login.action" filters="none" /> <!-- 商品管理 --> <!--限制具有ROLE_GOODS权限的用户才能访问 (4.0后版本只能用hasRole("ROLE_GOODS")--> <sec:intercept-url pattern="/admin/goods!**" access="ROLE_GOODS" /> <!-- 基础管理权限 --> <!--拦截/admin下的所有请求--> <sec:intercept-url pattern="/admin/**" access="ROLE_BASE" /> </sec:http> <sec:http auto-config="true" use-expressions="true"> <!-- 配置为permitAll允许所有已登录或者未登录用户访问,但依然经过过滤器处理。除此之外所有的页面都需要登录访问 --> <sec:intercept-url pattern="/" access="permitAll" /> <sec:intercept-url pattern="/index*" access="permitAll" /> <sec:intercept-url pattern="/signin*" access="permitAll" /> <sec:intercept-url pattern="/login*" access="permitAll" /> <sec:intercept-url pattern="/register*" access="permitAll" /> <sec:intercept-url pattern="/invalidsession*" access="permitAll" /> <sec:intercept-url pattern="/404*" access="none" /> <sec:access-denied-handler error-page="/403" /> <!-- 关闭跨域请求,默认开启,最好关闭 --> <security:csrf disabled="true"/> </sec:http> <!--角色继承--> <bean id="roleHierarchy" class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl"> <property name="hierarchy"><!-- 角色继承关系 --> <value> ROLE_ADMIN > ROLE_USER //ADMIN继承USER的所有权限,后者能访问的资源它也可以 ROLE_A > ROLE_B ROLE_B > ROLE_C ROLE_C > ROLE_D </value> </property> </bean>
http元素将创建一个FilterChainProxy以及链中的所有过滤器的bean,同时会将FilterChainProxy的bean托管给配置在web.xml中的DelegatingFilterProxy。
登录验证
auto-config=”true”自动加载的同时生成默认登录页面,相当于如下配置:
<http> <form-login/> <!--HTTP 基本认证 --> <http-basic/> <!-- 可以通过logout-url属性设置用户退出的url--> <logout /> <intercept pattern="/**" access="ROLE_DEMO" /> </http>
- 登录请求提交地址还是登录页面的地址,但是过滤器(不管是否自定义)会把参数获取过去验证。
一个简单的例子
<!--过滤器,拦截/admin下的所有请求,限制具有admin权限的用户才能访问--> <sec:http auto-config="true"> <!--true则自动生成一个登陆页面,url:localhost:8080/project/login--> <sec:intercept-url pattern="/**" access="authenticated" /> <!--访问任何页面都跳转登录界面--> </sec:http> <!-- 安全认证管理,先写个简单的后面再改--> <sec:authentication-manager> <sec:authentication-provider> <sec:user-service> <!--设置用户库中的用户;用此用户登录才能成功--> <sec:user name="admin" password="123456" authorities="ROLE_USER"/> </sec:user-service> </sec:authentication-provider> </sec:authentication-manager>
- 登录页面
- 登录失败转回登录页面
- 登录成功则进入原本要访问的页面或默认主页
虽然设置auto-config=”true”可以自动生成登录页面,但是一般都是使用自定义的登录页面
<http auto-config="true"> <!-- 设置登录页配置 login-page指定了登录界面的视图,authentication-failure-url则设置失败后的重定向到相同的登录页面--> <from-login login-processing-url="/static/j_spring_security_check" login-page="/login" authentication-failure-url="/login?login_error=t" username-parameter="sescs_username" password-parameter="sescs_password"> </http>
- 在自定义的登录页面中将表单提交地址定为“/static/j_spring_security_check”(3.x默认路径,而4.x则是/login),未定义username和password参数名的话将用户名和密码输入框的name分别设置为j_username和j_password。(4.x已定义为username、password)
- 自定义时应设表单提交路径与登录页面路径不同,应为相同的话万一前面配置了不拦截对登录页面的请求,那就相当于也不拦截白哦单提交请求了,那还怎么获取表单来验证。
实现记住密码功能
<!--key设置cookie的秘钥的值,默认是SpringSecured。后一个属性指定有效期 --> <remember-me key="spitterKey" token-validity-seconds="2419200"/>
则前端页面相应要有记住密码勾选框
<input name="_spring_security_rember_me" type="checkbox"/>
强制使用https:保证每次对指定url请求都会自动重定向为https请求,不管用户访问时是否加入https
<intercept pattern="/admin/**" access="ROLE_DEMO" requires-channel="https" />
-
使用SpEL
<!--use-expressions="true":声明使用表达式--> <http auto-config="true" use-expressions="true"> <!--需要ROLE_ADMIN权限才能访问--> <intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')"> <intercept-url pattern="/**" access="hasAnyRole('ROLE_ADMIN','ROLE_USER')" /> </http>
- 所支持的SpEL如下
- 补充一个authenticated,应该和第一个一样
- isAuthenticated():是否认证过
jsp标签
Spring Security提供jsp标签库,包含三个标签
- security:accesscontrollist :如果认证用户具有权限列表中的某一个权限,那么这个标签范围的内容将显示
- security:authentication: 访问当前用户认证对象的属性。一般用户显示当前用户的用户名之类的。具有的用户认证信息有:
- authorities:一组用于用户所授予的GrantedAuthority对象
- credentials:核实用户的凭据
- detail:认证的附加信息(IP地址,会话ID等)
- principal:用户的主要信息对象
security:authorize: 如果当前用户满足特定权限,则显示标签范围的内容
<!-- 显示用户信息, 并将信息复制给var变量,该变量的使用范围为scope的范围--> Hello <security:authentication property="principal.usrname" var="loginId" scope="request"> <security:authorize access="hasRole('ROLE_ADMIN')"> 如果当前用户有ROLE_ADMIN权限,则显示这部分内容 </security:authorize> <!--或者--> <security:authorize url="/admin/**"> 如果当前用户有/admin/**对应的权限,则显示这部分内容 </security:authorize>
认证用户
前面提到很多用户权限的问题,那么如何获取用户的权限呢?最常用的方式是查询数据库中相应用户的权限,前面登录验证的例子使用了内存用户存储库,参考文章也介绍了内存用户存储库的配置。
<jdbc-user-service id="userService" data-source-ref="dataSource" users-by-username="select username,password,true from user where username=?" authories-by-username-query="select username,role from user_role where username=?" /> <authentication-manager> <!--ref:直接注入bean;user-service-ref:先将bean注入一个叫DaoAuthenticationProvider的bean 的变量userDetailsService中,再将DaoAuthenticationProvider注authentication-provider入--> <authentication-provider user-service-ref="userService"/> </authentication-manager> <!--或者--> <authentication-manager erase-credentials="false"> <authentication-provider> <!--如果想用传统的md5或sha的话 <password-encoder hash="sha" /> --> <password-encoder ref="bcryptEncoder" /> <jdbc-user-service data-source-ref="dataSource" users-by-username-query="select username,password from user where username=?" authorities-by-username-query="select username,role from user_role where username=?"/> </authentication-provider> </authentication-manager> <!-- 配置加密的算法,表明在登录或密码需要加密验证; 生成的密文有60位,且每次都不同,所以数据库密码字段至少60位 --> <beans:bean name="bcryptEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
- 不写sql语句的话,Spring Security会自动生成sql语句从数据库中查找用户和权限,但一般情况下提供的查询语句并不能和我们的数据库对上,所以我们需要自己写,主要包括以下属性:
提供方法级别的保护,基于Spring AOP.首先需要applicationContext.xml中加入以下配置
<!--使spring Security保护那些带相关注解的方法--> <global-method-security secured-annotations="enabled" />
支持4种方法级别安全性的保护方式:
- 使用@Secured注解方法,这是spring自带的注解方法。@Secured(””)内部的字符串不具有SpEL特性,只能是具体的权限。
- 使用@JSR-250 @RelosAllowed注解的方法。作用和使用方法与@Secured一样,不同在于它不是spring框架的,所以可以做到和spring框架的解耦。
- 使用Spring 方法调用前和调用后注解方法。这些方法支持SpEL.
匹配一个或多个明确声明的切点方法。
@Secured("ROLE_ADMIN") public void addUser(User user){ ... } @RolesAllowed("ROLE_ADMIN") public void updateUser(User user){ ... } //匹配一个或多个明确声明的切点方法 <global-method-security secured-annotations="enabled" > <protect-pointcut access="ROLE_ADMIN" expression="execution(@com.securitytest.service.UserService**.*(String)" </global-method-security>
处理方法调用前和调用后的Spring注解(可以使用SpEL)
- @PreAuthorize: 在方法调用前,基于表达式计算结果来限制方法访问
- @PostAuthorize: 允许方法调用,但是如果表达式结果为fasle则抛出异常
- @PostFilter :允许方法调用,但必须按表达式过滤方法结果。
@PreFilter:允许方法调用,但必须在进入方法前过滤输入值
@PreAuthorize("hasRole('ROLE_ADMIN')") public void addUser(User user){ //如果具有权限 ROLE_ADMIN 访问该方法 .... } //returnObject可以获取返回对象user,判断user属性username是否和访问该方法的用户对象的用户名一样。不一样则抛出异常。 @PostAuthorize("returnObject.user.username==principal.username") public User getUser(int userId){ //允许进入 ... return user; } //将结果过滤,即选出性别为男的用户 @PostFilter("returnObject.user.sex=='男' ") public List<User> getUserList(){ //允许进入 ... return user; }
参考文章
- Spring Security入门
- SpringMvc整合Spring Security
- web.xml是最好的
- SpringMVC+Spring Security实现登录认证的简单功能
代码实例
- D:/ideaprojects/thz/thz-parent/thz-manager-web
Java Configuration方式参见笔记:Spring Security之Java Configuration方式